Skip to content

Security Extension Application Hardening

Now it is time to harden and protect your application against malicious attackers and from potential vulnerabilities. In order to add an additional layer of security in your application, remember the security that concepts we introduced in previous sections, such as setting up a container with read-only file system, initializing a container, adding/dropping Linux capabilities, and using a non-root user.

Mount read-only volume on MQTT broker

Using a read-only volume on MQTT broker protects you against an attacker modifying your MQTT credentials, even if the attacker has privileged user rights.

To show you how to mount a read-only volume on the container, we will use the example of MQTT by adding the following line in the MQTT docker-compose file:

# Open the docker-compose.yml from /src/solution/HandsOn_2/mqtt_broker_mosquitto

version: '2.4'                                # docker-compose version is set to 2.4

services:

    mqtt-broker:
      image: eclipse-mosquitto:$MQTT_VERSION  # define image to pull from docker hub if not already available on your machine
      container_name: ie-databus              # Name of MQTT broker container
      restart: unless-stopped                 # always restarts (see overview on page 12 of Industrial Edge Developer Guide)
      read_only: true                         # Read-only file system
      logging:                                # allow logging
        options:                              # we use best practice here as limiting file size and rolling mechanism
          max-size: "10m"                     # File size is 10MB
          max-file: "2"                       # only 2 files created before rolling mechanism applies
      volumes:                                # mount volume from host
        - mosquitto:/mosquitto:ro             # set to read-only volume
      ports:                                  # expose ports and publish
        - "33083:1883"                        # map containers default MQTT port (1883) to host's port 33083
      networks:                               # define networks connected to container 'mqtt-broker'
        proxy-redirect:                       # Name of the network

###### NETWORK CONFIG ######

networks:                           # Network interface configuration
  proxy-redirect:                   # Reference 'proxy-redirect' as predefined network
    name: proxy-redirect
    driver: bridge

###### VOLUMES ######

volumes:                            # Volumes for containers
  mosquitto:

Now you can build the docker-compose file again and run the MQTT container:

# Start the docker-compose file

docker-compose up --build

# Open another terminal


# Check if the mounted volume is read-only

docker ps
>>> CONTAINER ID   IMAGE                      COMMAND                  CREATED         ...
>>> aab72de9946f   eclipse-mosquitto:1.6.14   "/docker-entrypoint.…"   2 minutes ago   ...

docker inspect aab72de9946f
>>> ...
>>> {
>>>     "Type": "volume",
>>>     "Name": "mqtt_broker_mosquitto_mosquitto",
>>>     "Source": "/var/lib/docker/volumes/mqtt_broker_mosquitto_mosquitto/_data",
>>>     "Destination": "/mosquitto",
>>>     "Driver": "local",
>>>     "Mode": "z",
>>>     "RW": false,
>>>     "Propagation": ""
>>> }

As it shows in the terminal, the read-write operation ("RW") is set to false, which means the mounted volume is read-only.

Setup initial container in Influxdb

In the earlier docker section, we discussed how to initialize a container with a relative easy example. Using init containers, you can load secrets from various sources and initialize your application. You can also create a bad practice warning when a developer tries to hardcode the username and password in plain files. For the sake of simplicity, we do not discuss these details in the developer guide.

In the Hands On section, we will use the concept to initialize the influxdb container environment.

# Open the docker-compose.yml from /src/solution/HandsOn_2/my_edge_app

...

##### INFLUXDB ######

  init-db:
    build:
      context: ./influxdb
    image: init-db:v0.0.1               # Docker Init Container to setup the Main InfluxDB v2.2 Container
    depends_on:
      influxdb:
        condition: service_healthy
    networks:                           # define networks connected to container 'influxdb'
      proxy-redirect:                   # Name of the network

  influxdb:
    image: influxdb:2.2-alpine          # Define image to pull from docker hub if not already available on your machine
    container_name: influxdb            # Name of the influx-db container
    restart: unless-stopped             # always restarts (see overview on page 12 of Industrial Edge Developer Guide)
    mem_limit: 350m
    logging:                            # allow logging
      options:                          # we use best practice here as limiting file size and rolling mechanism
        max-size: "10m"                 # File size is 10MB
        max-file: "2"                   # only 2 files created before rolling mechanism applies
      driver: json-file
    ports:                              # expose ports and publish
      - "8086:8086"                     # map containers port 8086 to host's port 8086
    volumes:                            # mount volume from host
      - db-backup:/var/lib/influxdb     # mount named volume 'db-backup' to host's path to /var/lib/influxdb
    healthcheck:
      test: "exit 0"
    networks:                           # define networks connected to container 'influxdb'
      proxy-redirect:                   # Name of the network
...

The init-db container starts after the influxdb container and exits after executing the init_influxdb.sh bash script. The bash script sets up the user, credentials, org, retention period, database, and token automatically. The influxdb container service checks if the container is in a healthy status, and it will exit as 0. If so, the init-db container service starts after 30 seconds, because the check "exit 0" is triggered after 30 seconds as it is the default.

Info

For more information see Healthcheck section of the docker documentation.

# Start docker-compose file under /src/solution/HandsOn_2/my_edge_app

docker-compose up

You can access the influxdb from https://localhost:38086 with the following credentials: user: edge password: edgeadmin

You can now verify that the account settings were set up properly.

Select and drop docker capabilities

In the previous section we discussed the docker capabilities and their implications for container security at runtime. In this section, we will be using the examples of nginx and grafana containers to show how to select and drop unnecessary docker capabilities, in order to achieve fine-grained control of security capabilities.

Use of Tracee for Nginx containers

Use the Tracee tool to list the system calls being generated by a container. The tool can also be used to trace out cap_capable events, showing the capabilities that a newly created container requests from the kernel.

# Open a new terminal


# Go into the Hands-on-App directory src/solution/HandsOn_2/tracee

cd src/solution/HandsOn_2/tracee

# Start docker-compose file

docker-compose up

# Output

>>> INFO: probing tracee-ebpf capabilities...
>>> TIME    CONTAINER_ID    UID    COMM    PID/host    TID/host    RET    EVENT    ARGS

As an example, we will show the first few events traced from a nginx container.

# Open another terminal


# Go into the src/solution/HandsOn_2/example

cd ../example

# Start docker-compose file

docker-compose -f docker-compose.web.yml up --build

# Open browser on

http://localhost:8080

# After it runs successfully, you can close the container

docker-compose down

You can see the following outputs from the Tracee terminal. You can also find the logs in /var/lib/docker/container/container_id or you can execute command docker logs tracee in terminal to get the logs.

INFO: probing tracee-ebpf capabilities...
TIME             CONTAINER_ID    UID      COMM             EVENT               ARGS
13:59:03:600324  a71789d1a0e1      0      nginx            cap_capable         cap: CAP_DAC_READ_SEARCH
13:59:03:603116  a71789d1a0e1      0      nginx            cap_capable         cap: CAP_CHOWN
13:59:03:605167  a71789d1a0e1      0      nginx            cap_capable         cap: CAP_DAC_OVERRIDE
13:59:03:609840  a71789d1a0e1      0      nginx            cap_capable         cap: CAP_SETGID
13:59:03:613501  a71789d1a0e1      0      nginx            cap_capable         cap: CAP_SETUID

Once you know which capabilities your container needs, you can follow the principle of least privilege and specify at runtime the precise set that should be granted. The recommended approach is to drop all capabilities and then add back the necessary ones as follows:

# Open and edit the docker-compose.web.yml

version: '2.4'

services:
    web:
      image: nginx
      volumes:
      - ./templates:/etc/nginx/templates
      ports:
      - "8080:80"
      environment:
      - NGINX_HOST=foobar.com
      - NGINX_PORT=80
      cap_drop:                                         # Drop all linux capabilities
        - ALL
      cap_add:                                          # Select only the necessary capabilities from tracee tool
        - DAC_READ_SEARCH
        - DAC_OVERRIDE
        - CHOWN
        - SETGID
        - SETUID

Now run the docker-compose file again

# Under the same terminal


# Start docker-compose file

docker-compose -f docker-compose.web.yml up --build

# Open browser on

http://localhost:8080

By selecting the necessary capabilities, the nginx container runs successfully without any problems, even if we drop all other docker capabilities.

Rinse-and-repeat strategy

Based on the selected capabilities, you can also test and drop other added capabilities one by one until the container throws permission errors. In the nginx case you do not need CAP_DAC_OVERRIDE and CAP_DAC_READ_SEARCH because the nginx container starts as root, which has CAP_DAC_OVERRIDE capability by default. However it is not needed during runtime.

In conclusion, you can edit the docker-compose file as follows:

# Open and edit the docker-compose.web.yml

version: '2.4'

services:
    web:
      image: nginx
      volumes:
      - ./templates:/etc/nginx/templates
      ports:
      - "8080:80"
      environment:
      - NGINX_HOST=foobar.com
      - NGINX_PORT=80
      cap_drop:                                         # Drop all Linux capabilities
        - ALL
      cap_add:                                          # Select only the necessary capabilities from Tracee tool
        - CHOWN
        - SETGID
        - SETUID

Implement a non-root user

In the previous section, we discussed how a non-root user works in docker. As an example, we will use the data-analytics container to initiate the container with a non-root user.

# Open the Dockerfile from /src/solution/HandsOn_2/my_edge_app/data-analytics

ARG BASE_IMAGE
FROM ${BASE_IMAGE}

# Add non-root user

RUN adduser -S nonroot

# install all requirements from requirements.txt

COPY requirements.txt /
RUN pip install -r /requirements.txt; rm -f /requirements.txt

# Set the working directory to /app

WORKDIR /app

# Copy the current dir into the container at /app

COPY ./program/* /app/

# Use the

USER nonroot

# Run app.py when the container launches

CMD ["python", "-u", "-m", "app"]

Now you can build the docker-compose file again and run it with Node-RED and MQTT containers:

# In the /src/solution/HandsOn_2/mqtt_broker_mosquitto directory

docker-compose up

# In the /src/solution/HandsOn_2/my_edge_app directory

docker-compose up -build

# In the /src/solution/HandsOn_2/node_red directory

docker-compose up

# Check which user the data-analytics container is running

docker ps
>>> CONTAINER ID   IMAGE                      COMMAND             ...
>>> abcbf7de7e6f   data-analytics:v0.0.1      "python -u -m app"  ...

docker exec -it abcbf7de7e6f /bin/sh
/app $ whoami
>>> nonroot

# Open another terminal

docker inspect --format '{{.State.Pid}}' abcbf7de7e6f
>>> 24957

# There is no capability available in the container

getpcaps 24957
>>> 24957:=

You can see from the terminal that the data-analytics container is running as a non-root user and has no Linux capability, while the application runs without problems.

This was a brief overview of the main points of the Python app which is contained in 'my_edge_app'.

Overview of the Docker-Compose file after hardening

The docker-compose.yml is used to build the containers. An overview of docker-compose.yml after security hardening is shown below.

Picture_5-56_security_extension_docker-compose-yml